using System;
using System.Collections;
using PathX.Collection;
using PathX.Unity;
using UnityEngine;
using FPLibrary;

namespace PathX
{
    public abstract class PathingEngineBase : IPathingEngine
    {
        private IPathNode _goal;
        private PathResult _currentResult;
        private SegmentRequest _segmentRequest;
        private DynamicArray<Path> _segments;
        protected IMoveCost costProvider;
    
        /// <summary>
        /// Initializes a new instance of the <see cref="PathingEngineBase"/> class.
        /// </summary>
        /// <param name="moveCostProvider">The move cost provider.</param>
        /// <param name="cellCostStrategy">The cell cost provider.</param>
        /// <param name="smoother">The path smoother to use</param>
        /// <param name="requestPreprocessors">The list of request preprocessors to use.</param>
        protected PathingEngineBase(IMoveCost cost)
        {
            _segmentRequest = new SegmentRequest();
            _segments = new DynamicArray<Path>(10);
            costProvider = cost;
        }
        
        /// <summary>
        /// Gets the current request.
        /// </summary>
        /// <value>
        /// The current request.
        /// </value>
        protected IPathRequest currentRequest
        {
            get { return _segmentRequest; }
        }
    
        /// <summary>
        /// Gets the goal, i.e. final destination.
        /// </summary>
        /// <value>
        /// The goal.
        /// </value>
        protected IPathNode goal
        {
            get { return _goal; }
        }

        /// <summary>
        /// Processes the request.
        /// </summary>
        /// <param name="request">The request.</param>
        public void ProcessRequest(IPathRequest request)
        {
            try
            {
                //Granted this could be done smarter, but since this is not an expected scenario, we just use exception handling to handle it.
                ValidateRequest(request);
    
                _currentResult = new PathResult(PathingStatus.Complete, null, 0, request);
    
                var status = PathingStatus.Complete;
    
                //Iterate over the waypoints of the request, including the end point
                _segmentRequest.Start(request);
    
                do
                {
                    status = StartPathSegment();
                    if (status != PathingStatus.Running)
                    {
                        break;
                    }
    
                    while (status == PathingStatus.Running)
                    {
                        status = ProcessNext();
                    }
    
                    if (!CompletePathSegment(status))
                    {
                        break;
                    }
                }
                while (_segmentRequest.MoveNext());
    
                if (status != PathingStatus.Complete)
                {
                    if (_segments.count > 0)
                    {
                        _currentResult.RegisterPartialResult(status, _segmentRequest.GetPendingWaypoints());
                    }
                    else
                    {
                        _currentResult.status = status;
                    }
                }
    
                //Splice the path segments and complete the request
                _currentResult.path = Path.FromSegments(_segments);
                request.Complete(_currentResult);
            }
            catch (Exception e)
            {
                FailRequest(request, e);
            }
            finally
            {
                _segments.Clear();
                _segmentRequest.Clear();
                _currentResult = null;
            }
        }
        
        protected void UpdateGoal(IPathNode newGoal)
        {
            _goal = newGoal;
            _segmentRequest.to = newGoal.position;
        }

        protected virtual void ValidateRequest(IPathRequest request)
        {
            Ensure.ArgumentNotNull(request, "request");
    
            if (!request.isValid)
            {
                throw new ArgumentException("The request is invalid.", "request");
            }
    
            if (_segmentRequest.isRunning)
            {
                throw new InvalidOperationException("A new request cannot be started while another request is being processed.");
            }
        }

        private PathingStatus StartPathSegment()
        {
            var fromGrid = PathGridManager.Instance.GetGrid(_segmentRequest.from);
            var toGrid = PathGridManager.Instance.GetGrid(_segmentRequest.to);
    
            var fromPos = new Position(_segmentRequest.from);
            var toPos = new Position(_segmentRequest.to);
    
            //If no grids were resolved for this request it means the request involves two points outside the grid(s) that do not cross any grid(s), so we can move directly between them
            if (fromGrid == null && toGrid == null)
            {
                var pathSegment = new Path(2);
                pathSegment.Push(toPos);
                pathSegment.Push(fromPos);
                _segments.Add(pathSegment);
                _currentResult.pathCost = costProvider.GetHeuristic(fromPos, toPos);
                return PathingStatus.Complete;
            }
            else if (fromGrid == null)
            {
                return PathingStatus.StartOutsideGrid;
            }
            else if(toGrid == null || toGrid != fromGrid)  //项目不做跨Grid的寻路支持
            {
                return PathingStatus.Failed;
            }
    
            var options = _segmentRequest.pathFinderOptions;
            // if (fromGrid == toGrid && !options.preventDiagonalMoves)
            // {
            //     var pathSegment = new Path(2);
            //     pathSegment.Push(toPos);
            //     pathSegment.Push(fromPos);
            //     _segments.Add(pathSegment);
            //     _currentResult.pathCost = costProvider.GetHeuristic(fromPos, toPos);
            //     return PathingStatus.Complete;
            // }
    
            //Get a reference to the start and end cells
            var start = fromGrid.GetCell(_segmentRequest.from, false) as IPathNode;
            _goal = toGrid.GetCell(_segmentRequest.to, false) as IPathNode;
    
            //Ensure that the end cell is valid
            if (_goal == null)
            {
                if (options.navigateToNearestIfBlocked)
                {
                    _goal = toGrid.GetCell(_segmentRequest.to, true) as IPathNode;
                    _segmentRequest.to = _goal.position;
                }
                else
                {
                    return PathingStatus.EndOutsideGrid;
                }
            }
    
            if (!_goal.IsWalkableWithClearance(_segmentRequest.requesterProperties) && !options.navigateToNearestIfBlocked)
            {
                return PathingStatus.DestinationBlocked;
            }
    
            //Ensure that the start cell is valid
            IPathNode actualStart = null;
            if (!start.IsWalkableWithClearance(_segmentRequest.requesterProperties))
            {
                actualStart = start;
                start = fromGrid.GetNearestWalkableCell(start.position, _goal.position, true, options.maxEscapeCellDistanceIfOriginBlocked, _segmentRequest.requesterProperties);
                if (start == null)
                {
                    return PathingStatus.NoRouteExists;
                }
            }
    
            OnStart(start, actualStart);
    
            return PathingStatus.Running;
        }

        private bool CompletePathSegment(PathingStatus status)
        {
            if (status != PathingStatus.Complete)
            {
                return false;
            }

            _currentResult.pathCost += _goal.g;

            //Fix the actual destination so it does not overlap obstructions
            FixupGoal(_segmentRequest);
            
            var maxPathLength = MathfFix64.CeilToInt(_goal.g / (_goal.parent.cellSize * costProvider.baseMoveCost));

            var pathSegment = new Path(maxPathLength);

            //Push the actual end position as the goal
            pathSegment.Push(new Position(_segmentRequest.to));

            IPathNode current = _goal.predecessor;
            while (current != null)
            {
                pathSegment.Push(current);
                current = current.predecessor;
            }

            //Instead of testing for it in the while loop, just pop off the start node and replace it with the actual start position
            if (pathSegment.count > 1)
            {
                pathSegment.Pop();
            }
            pathSegment.Push(new Position(_segmentRequest.from));
            
            _segments.Add(pathSegment);

            return true;
        }

        private void FailRequest(IPathRequest request, Exception e)
        {
            _segments.Clear();
            _segmentRequest.Clear();
            _currentResult = null;
    
            var result = new PathResult(PathingStatus.Failed, null, 0, request)
            {
                errorInfo = string.Concat(e.Message, Environment.NewLine, e.StackTrace)
            };
    
            request.Complete(result);
        }
    
        private void FixupGoal(IPathRequest request)
        {
            var unitProps = request.requesterProperties;
            var requesterRadius = request.requesterProperties.radius;
            var actualGoal = request.to;
    
            var halfCell = _goal.parent.cellSize * 0.5f;
    
            var dx = actualGoal.x - _goal.position.x;
            var dz = actualGoal.z - _goal.position.z;
    
            var overlapLeft = (requesterRadius - dx) - halfCell;
            var overlapRight = (dx + requesterRadius) - halfCell;
            var overlapTop = (dz + requesterRadius) - halfCell;
            var overlapBottom = (requesterRadius - dz) - halfCell;
    
            Fix64 adjX = 0.0f;
            Fix64 adjZ = 0.0f;
    
            if (overlapLeft > 0.0f && !ValidateGoalNeighbour(unitProps, -1, 0))
            {
                adjX = -overlapLeft;
            }
            else if (overlapRight > 0.0f && !ValidateGoalNeighbour(unitProps, 1, 0))
            {
                adjX = overlapRight;
            }
    
            if (overlapTop > 0.0f && !ValidateGoalNeighbour(unitProps, 0, 1))
            {
                adjZ = overlapTop;
            }
            else if (overlapBottom > 0.0f && !ValidateGoalNeighbour(unitProps, 0, -1))
            {
                adjZ = -overlapBottom;
            }
    
            //If we do overlap direct neighbours but they are free we have to ensure that the diagonal is also free.
            if ((adjX == 0.0f) && (adjZ == 0.0f))
            {
                if ((overlapLeft > 0.0f) && (overlapTop > 0.0f) && !ValidateGoalNeighbour(unitProps, -1, 1))
                {
                    adjX = -overlapLeft;
                    adjZ = overlapTop;
                }
                else if ((overlapLeft > 0.0f) && (overlapBottom > 0.0f) && !ValidateGoalNeighbour(unitProps, -1, -1))
                {
                    adjX = -overlapLeft;
                    adjZ = -overlapBottom;
                }
                else if ((overlapRight > 0.0f) && (overlapTop > 0.0f) && !ValidateGoalNeighbour(unitProps, 1, 1))
                {
                    adjX = overlapRight;
                    adjZ = overlapTop;
                }
                else if ((overlapRight > 0.0f) && (overlapBottom > 0.0f) && !ValidateGoalNeighbour(unitProps, 1, -1))
                {
                    adjX = overlapRight;
                    adjZ = -overlapBottom;
                }
            }
    
            if ((adjX != 0.0f) || (adjZ != 0.0f))
            {
                request.to = new FPVector(actualGoal.x - adjX, actualGoal.y, actualGoal.z - adjZ);
            }
        }
    
        private bool ValidateGoalNeighbour(IUnitProperties unitProps, int dx, int dz)
        {
            var c = _goal.GetNeighbour(dx, dz);
            return ((c == null) || c.IsWalkableFrom(_goal, unitProps));
        }
        
        protected abstract PathingStatus ProcessNext();
        
        protected virtual void OnStart(IPathNode start, IPathNode actualStart)
        {
        }
    
        private class SegmentRequest : IPathRequest
        {
            private IPathRequest _actualRequest;
            private FPVector _actualFrom;
            private FPVector _actualTo;
            private int _viaIdx;
    
            public FPVector from
            {
                get { return _actualFrom; }
                set { _actualFrom = value; }
            }
    
            public FPVector to
            {
                get { return _actualTo; }
                set { _actualTo = value; }
            }
    
            public FPVector[] via
            {
                get { return null; }
                set {  /* NOOP */ }
            }
    
            public bool isRunning
            {
                get { return _actualRequest != null; }
            }

            public IUnitProperties requesterProperties
            {
                get { return _actualRequest.requesterProperties; }
                set { _actualRequest.requesterProperties = value; }
            }
            
            public IPathFinderOptions pathFinderOptions
            {
                get { return _actualRequest.pathFinderOptions; }
                set { _actualRequest.pathFinderOptions = value; }
            }

            public float timeStamp
            {
                get;
                set;
            }
    
            public bool isValid
            {
                get { return _actualRequest.isValid; }
            }
    
            public bool hasDecayed
            {
                get { return _actualRequest.hasDecayed; }
                set { _actualRequest.hasDecayed = value; }
            }
    
            public object customData
            {
                get { return _actualRequest.customData; }
                set { _actualRequest.customData = value; }
            }
    
            public void Complete(PathResult result)
            {
                throw new InvalidOperationException();
            }
    
            public void Start(IPathRequest req)
            {
                _actualRequest = req;
                _actualFrom = req.from;
    
                if (req.via != null && req.via.Length > 0)
                {
                    _viaIdx = 0;
                    _actualTo = req.via[0];
                }
                else
                {
                    _viaIdx = -1;
                    _actualTo = req.to;
                }
            }
    
            public bool MoveNext()
            {
                if (_viaIdx < 0)
                {
                    _actualRequest.to = _actualTo;
                    return false;
                }
    
                var via = _actualRequest.via;
                via[_viaIdx++] = _actualTo;
    
                _actualFrom = _actualTo;
    
                if (_viaIdx == via.Length)
                {
                    _actualTo = _actualRequest.to;
                    _viaIdx = -1;
                }
                else
                {
                    _actualTo = via[_viaIdx];
                }
    
                return true;
            }
    
            public FPVector[] GetPendingWaypoints()
            {
                if (_viaIdx < 0)
                {
                    return new FPVector[] { _actualRequest.to };
                }
    
                var via = _actualRequest.via;
                var remaining = (via.Length - _viaIdx);
                var arr = new FPVector[remaining + 1];
                Array.Copy(via, _viaIdx, arr, 0, remaining);
                arr[remaining] = _actualRequest.to;
    
                return arr;
            }
    
            public void Clear()
            {
                _actualRequest = null;
            }
        }
    }
}